<?php
/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

namespace Al\HTTP\Lib;

use Al\Core\Lib\Service;

/**
 * A route configuration.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 */
class Route {

  /**
   * @var string The path that this route will match
   */
  public $path;

  /**
   * @var string Controller class name
   */
  public $controller;

  /**
   * @var string Controller method name
   */
  public $action;

  /**
   * @var bool True to catch the response into a static storage
   */
  public $cache;

  /**
   * @var string Route name
   */
  protected $name;

  /**
   * @var bool|string
   */
  protected $method;

  /**
   * @var array A collection of parameter names
   */
  protected $params;

  /**
   * @var array Default parameter values
   */
  protected $defaults;

  /**
   * @var array Parameter regex validation requirements
   */
  protected $requirements;

  /**
   * Constructor
   *
   * @param string $name A route name
   * @param array $config A configuration array
   */
  public function __construct($name, $config) {
    $this->name = $name;
    $this->controller = $config['controller'];
    $this->action = $config['action'];
    $this->path = $config['path'];
    $this->params = $config['params'];
    $this->method = $config['method'];
    $this->defaults = $config['defaults'];
    $this->requirements = $config['requirements'];
    $this->cache = $config['cache'];
  }

  /**
   * Matches a path against this route and returns an array of parameter values.
   * Returns boolean false if the path does not match.
   * Caution. May return an empty array, which is a falsy value, always perform
   * a strict comparison.
   *
   * Example:
   * <code>
   *   if (false !== ($result = $route->match('/blog/post/1'))) {
   *     // the path matches, use $result here
   *   }
   * </code>
   *
   * @param string $path
   * @return array|bool
   */
  final public function match($path) {
    $values = array();
    preg_match_all('/^' . str_replace('/', '\/', preg_replace('/{[^}]+}/',
        '(.+)?', $this->path)) . '$/', $path, $values);
    if (count($values[0]) && (!$this->method || ($this->method === strtolower(
        Service::get('http.server.REQUEST_METHOD'))))) {
      $result = array();
      foreach ($this->defaults as $key => $value) {
        $result[$key] = $value;
      }
      foreach ($this->params as $key => $value) {
        $result[$value] = $values[$key + 1][0];
      }
      foreach ($this->requirements as $key => $value) {
        if (!preg_match("/^$value$/", $result[$key])) return false;
      }
      return $result;
    }
    return false;
  }

  /**
   * Attempts to build a path string based on a route configuration,
   * substituting all the route parameters with the ones provided in the
   * $params or if such a value is missing tries to fallback to the defaults
   * specified in the route and finally attempts to read it from the session.
   * All the unused extra values from the $params array are added as query
   * string parameters.
   *
   * Example:
   * <code>
   * // Acme/Blog/Resources/config/config.yml
   * routes:
   *   blog_post_edit:
   *     path: "{_locale}/blog/post/{id}/edit"
   *     controller: Acme\Blog:post:edit
   *
   * $route = Service::get('config.routes.blog_post_edit');
   * // "/en/blog/post/5/edit"
   * $route->buildPath(array('id' => 5));
   * </code>
   *
   * @param array|\Traversable $params [optional] Route parameters.
   * @return string A path (eg. "/en/blog/post/5/edit").
   * @throws \InvalidArgumentException
   */
  final public function buildPath($params = array()) {
    $defaults = $this->defaults;
    $session = Service::get('http.session');
    $path = preg_replace_callback('/{([^}]*)}/', function ($matches)
      use (&$params, &$session, &$defaults) {
        if (isset($params[$matches[1]])) {
          $value = $params[$matches[1]];
          unset($params[$matches[1]]);
        } elseif (isset($defaults[$matches[1]])) {
          $value = $defaults[$matches[1]];
        } elseif (isset($session[$matches[1]])) {
          $value = $session[$matches[1]];
        } else {
          throw new \InvalidArgumentException('The route parameter "' .
            $matches[1] . '" has not been specified.');
        }
        return $value;
      }, $this->path);
    $request_id = md5(Service::get('http.server.address') . (count($params) ?
      "$path?" . http_build_query($params) : $path));
    if ($this->cache) {
      $routes = (array)include 'cache/routes.php';
      $params['__fmod'] = isset($routes[$request_id]) ?
        filemtime($routes[$request_id]['file']) : time();
    }
    return count($params) ? "$path?" . http_build_query($params) : $path;
  }

}